package app

import (
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"os"
	"path"
	"sync"
	"time"

	"gopkg.in/ldap.v2"
	"gopkg.in/yaml.v2"
)

type application struct {
  mu sync.RWMutex
  // cidr白名单 eg. 192.168.11.0/24
  Ignore       []string `yaml:"ignore"`
  Ldap struct {
    Protocol     string `yaml:"protocol"`
    Host         string `yaml:"host"`
    Port         int    `yaml:"port"`
    RootDN       string `yaml:"rootDN"`
    SearchOrigin string `yaml:"searchOrigin"`
  }
  TLS struct {
    InsecureSkipVerify bool   `yaml:"insecureSkipVerify"`
    minVersion         string `yaml:"minVersion"`
  }
}

func (a *application) String() string {
  return fmt.Sprintf("%s://%s:%d", a.Ldap.Protocol, a.Ldap.Host, a.Ldap.Port)
}

func (a *application) reload() {
  a.mu.RLock()
  defer a.mu.RUnlock()
  fd, err := os.OpenFile(path.Join("etc", "application.yml"), os.O_RDONLY|os.O_CREATE, 0644)
  if err != nil {
    log.Fatalln(err)
  }
  defer fd.Close()
  b, err := ioutil.ReadAll(fd)
  if err != nil {
    log.Fatalln(err)
  }
  if err := yaml.Unmarshal(b, a); err != nil {
    log.Fatalln(err)
  }
}

func (a *application) url() string {
  return fmt.Sprintf("%s:%d", a.Ldap.Host, a.Ldap.Port)
}
func (a *application) login(username string) string {
  return fmt.Sprintf("uid=%s,%s,%s", username, a.Ldap.SearchOrigin, a.Ldap.RootDN)
}
func (a *application) ipcheck(ipstr string) bool {
  ipaddr := net.ParseIP(ipstr)
  for _, v := range a.Ignore {
    _, ipnet, err := net.ParseCIDR(v)
    if err != nil {
      log.Println(err)
    } else if ipnet.IP.Equal(ipaddr.Mask(ipnet.Mask)) {
      return true
    }
  }
  return false
}
func (a *application) minTLSVersion() uint16 {
  switch a.TLS.minVersion {
    case "tls1.0":
      return tls.VersionTLS10
    case "tls1.1":
      return tls.VersionTLS11
    case "tls1.2":
      return tls.VersionTLS12
    case "tls1.3":
      return tls.VersionTLS13
    default:
      return tls.VersionTLS12
  }
}

func (a *application) fail(w http.ResponseWriter, req *http.Request) {
  w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
  http.Error(w, "Unauthorized", http.StatusUnauthorized)
  username, _, ok := req.BasicAuth()
  if ok {
    log.Printf("Auth fail from %s@%s\n", username, req.Header.Get("X-Real-IP"))
  } else {
    log.Printf("No basicAuth info from %s %s\n", req.RemoteAddr, req.Header.Get("X-Real-IP"))
  }
}

// Reload the config from etc/application.yml
func (a *application) Reload(w http.ResponseWriter, req *http.Request) {
  a.reload()
  w.WriteHeader(http.StatusOK)
  log.Printf("Reload from %s\n", req.RemoteAddr)
}

// LdapAuth
//     用ldap服务认证basicAuth请求信息
func (a *application) LdapAuth(w http.ResponseWriter, req *http.Request) {
  realip := req.Header.Get("X-Real-IP")
  if realip != "" {
    if a.ipcheck(realip) {
      w.WriteHeader(http.StatusOK)
      log.Printf("Auth ignore from %s\n", realip)
      return
    }
  }
  username, password, ok := req.BasicAuth()
  if ok {
    if username != "" && password != "" {
      var conn *ldap.Conn
      var err error
      if a.Ldap.Protocol == "ldap" {
        conn, err = ldap.Dial("tcp", a.url())
        if err != nil {
          log.Println(err)
          a.fail(w, req)
          return
        }
      } else {
        tc := &tls.Config{
          InsecureSkipVerify: a.TLS.InsecureSkipVerify,
          ServerName: a.Ldap.Host,
          MinVersion: a.minTLSVersion(),
        }
        conn, err = ldap.DialTLS("tcp", a.url(), tc)
        if err != nil {
          log.Println(err)
          a.fail(w, req)
          return
        }
      }

      defer conn.Close()

      err = conn.Bind(a.login(username), password)
      if err != nil {
        log.Println(err)
        a.fail(w, req)
        return
      }

      searchRequest := ldap.NewSearchRequest(a.login(username), ldap.ScopeWholeSubtree, 
         ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{"cn", "pwdChangedTime"}, nil)

      search, err := conn.Search(searchRequest)
      if err != nil {
        log.Println(err)
        a.fail(w, req)
        return
      }

      w.WriteHeader(http.StatusOK)
      for _, en := range search.Entries {
        t, err := time.ParseInLocation("20060102150405Z", en.GetAttributeValue("pwdChangedTime"), time.Local)
        if err != nil {
          log.Println(err)
        } else {
          fmt.Fprintln(w, t.Local())
        }
      }
      log.Printf("Auth ok from %s@%s\n", username, realip)
      return
    }
  } else {
    a.fail(w, req)
    return
  }
}

var App = &application{}

func init() {
  App.reload()
}
